// Copyright 2021 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package snap

import (
	"bytes"
	"crypto/rand"
	"encoding/binary"
	"fmt"
	"math/big"
	mrand "math/rand"
	"slices"
	"sync"
	"testing"
	"time"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/rawdb"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethdb"
	"github.com/ethereum/go-ethereum/internal/testrand"
	"github.com/ethereum/go-ethereum/log"
	"github.com/ethereum/go-ethereum/rlp"
	"github.com/ethereum/go-ethereum/trie"
	"github.com/ethereum/go-ethereum/trie/trienode"
	"github.com/ethereum/go-ethereum/triedb"
	"github.com/ethereum/go-ethereum/triedb/pathdb"
	"github.com/holiman/uint256"
	"golang.org/x/crypto/sha3"
)

func TestHashing(t *testing.T) {
	t.Parallel()

	var bytecodes = make([][]byte, 10)
	for i := 0; i < len(bytecodes); i++ {
		buf := make([]byte, 100)
		rand.Read(buf)
		bytecodes[i] = buf
	}

	var want, got string

	var old = func() {
		hasher := sha3.NewLegacyKeccak256()
		for i := 0; i < len(bytecodes); i++ {
			hasher.Reset()
			hasher.Write(bytecodes[i])
			hash := hasher.Sum(nil)
			got = fmt.Sprintf("%v\n%v", got, hash)
		}
	}

	var new = func() {
		hasher := crypto.NewKeccakState()
		var hash = make([]byte, 32)
		for i := 0; i < len(bytecodes); i++ {
			hasher.Reset()
			hasher.Write(bytecodes[i])
			hasher.Read(hash)
			want = fmt.Sprintf("%v\n%v", want, hash)
		}
	}

	old()
	new()

	if want != got {
		t.Errorf("want\n%v\ngot\n%v\n", want, got)
	}
}

func BenchmarkHashing(b *testing.B) {
	var bytecodes = make([][]byte, 10000)
	for i := 0; i < len(bytecodes); i++ {
		buf := make([]byte, 100)
		rand.Read(buf)
		bytecodes[i] = buf
	}

	var old = func() {
		hasher := sha3.NewLegacyKeccak256()
		for i := 0; i < len(bytecodes); i++ {
			hasher.Reset()
			hasher.Write(bytecodes[i])
			hasher.Sum(nil)
		}
	}

	var new = func() {
		hasher := crypto.NewKeccakState()
		var hash = make([]byte, 32)
		for i := 0; i < len(bytecodes); i++ {
			hasher.Reset()
			hasher.Write(bytecodes[i])
			hasher.Read(hash)
		}
	}

	b.Run("old", func(b *testing.B) {
		b.ReportAllocs()

		for i := 0; i < b.N; i++ {
			old()
		}
	})
	b.Run("new", func(b *testing.B) {
		b.ReportAllocs()

		for i := 0; i < b.N; i++ {
			new()
		}
	})
}

type (
	accountHandlerFunc func(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error
	storageHandlerFunc func(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error
	trieHandlerFunc    func(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error
	codeHandlerFunc    func(t *testPeer, id uint64, hashes []common.Hash, max uint64) error
)

type testPeer struct {
	id            string
	test          *testing.T
	remote        *Syncer
	logger        log.Logger
	accountTrie   *trie.Trie
	accountValues []*kv
	storageTries  map[common.Hash]*trie.Trie
	storageValues map[common.Hash][]*kv

	accountRequestHandler accountHandlerFunc
	storageRequestHandler storageHandlerFunc
	trieRequestHandler    trieHandlerFunc
	codeRequestHandler    codeHandlerFunc
	term                  func()

	// counters
	nAccountRequests  int
	nStorageRequests  int
	nBytecodeRequests int
	nTrienodeRequests int
}

func newTestPeer(id string, t *testing.T, term func()) *testPeer {
	peer := &testPeer{
		id:                    id,
		test:                  t,
		logger:                log.New("id", id),
		accountRequestHandler: defaultAccountRequestHandler,
		trieRequestHandler:    defaultTrieRequestHandler,
		storageRequestHandler: defaultStorageRequestHandler,
		codeRequestHandler:    defaultCodeRequestHandler,
		term:                  term,
	}
	//stderrHandler := log.StreamHandler(os.Stderr, log.TerminalFormat(true))
	//peer.logger.SetHandler(stderrHandler)
	return peer
}

func (t *testPeer) setStorageTries(tries map[common.Hash]*trie.Trie) {
	t.storageTries = make(map[common.Hash]*trie.Trie)
	for root, trie := range tries {
		t.storageTries[root] = trie.Copy()
	}
}

func (t *testPeer) ID() string      { return t.id }
func (t *testPeer) Log() log.Logger { return t.logger }

func (t *testPeer) Stats() string {
	return fmt.Sprintf(`Account requests: %d
Storage requests: %d
Bytecode requests: %d
Trienode requests: %d
`, t.nAccountRequests, t.nStorageRequests, t.nBytecodeRequests, t.nTrienodeRequests)
}

func (t *testPeer) RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes uint64) error {
	t.logger.Trace("Fetching range of accounts", "reqid", id, "root", root, "origin", origin, "limit", limit, "bytes", common.StorageSize(bytes))

	t.nAccountRequests++
	go t.accountRequestHandler(t, id, root, origin, limit, bytes)

	return nil
}

func (t *testPeer) RequestTrieNodes(id uint64, root common.Hash, paths []TrieNodePathSet, bytes uint64) error {
	t.logger.Trace("Fetching set of trie nodes", "reqid", id, "root", root, "pathsets", len(paths), "bytes", common.StorageSize(bytes))

	t.nTrienodeRequests++
	go t.trieRequestHandler(t, id, root, paths, bytes)

	return nil
}

func (t *testPeer) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes uint64) error {
	t.nStorageRequests++
	if len(accounts) == 1 && origin != nil {
		t.logger.Trace("Fetching range of large storage slots", "reqid", id, "root", root, "account", accounts[0], "origin", common.BytesToHash(origin), "limit", common.BytesToHash(limit), "bytes", common.StorageSize(bytes))
	} else {
		t.logger.Trace("Fetching ranges of small storage slots", "reqid", id, "root", root, "accounts", len(accounts), "first", accounts[0], "bytes", common.StorageSize(bytes))
	}

	go t.storageRequestHandler(t, id, root, accounts, origin, limit, bytes)

	return nil
}

func (t *testPeer) RequestByteCodes(id uint64, hashes []common.Hash, bytes uint64) error {
	t.nBytecodeRequests++
	t.logger.Trace("Fetching set of byte codes", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes))

	go t.codeRequestHandler(t, id, hashes, bytes)

	return nil
}

// defaultTrieRequestHandler is a well-behaving handler for trie healing requests
func defaultTrieRequestHandler(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error {
	// Pass the response
	var nodes [][]byte

	for _, pathset := range paths {
		switch len(pathset) {
		case 1:
			blob, _, err := t.accountTrie.GetNode(pathset[0])
			if err != nil {
				t.logger.Info("Error handling req", "error", err)
				break
			}

			nodes = append(nodes, blob)
		default:
			account := t.storageTries[(common.BytesToHash(pathset[0]))]
			for _, path := range pathset[1:] {
				blob, _, err := account.GetNode(path)
				if err != nil {
					t.logger.Info("Error handling req", "error", err)
					break
				}

				nodes = append(nodes, blob)
			}
		}
	}

	t.remote.OnTrieNodes(t, requestId, nodes)

	return nil
}

// defaultAccountRequestHandler is a well-behaving handler for AccountRangeRequests
func defaultAccountRequestHandler(t *testPeer, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error {
	keys, vals, proofs := createAccountRequestResponse(t, root, origin, limit, cap)
	if err := t.remote.OnAccounts(t, id, keys, vals, proofs); err != nil {
		t.test.Errorf("Remote side rejected our delivery: %v", err)
		t.term()

		return err
	}

	return nil
}

func createAccountRequestResponse(t *testPeer, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) (keys []common.Hash, vals [][]byte, proofs [][]byte) {
	var size uint64

	if limit == (common.Hash{}) {
		limit = common.MaxHash
	}

	for _, entry := range t.accountValues {
		if size > cap {
			break
		}

		if bytes.Compare(origin[:], entry.k) <= 0 {
			keys = append(keys, common.BytesToHash(entry.k))
			vals = append(vals, entry.v)
			size += uint64(32 + len(entry.v))
		}
		// If we've exceeded the request threshold, abort
		if bytes.Compare(entry.k, limit[:]) >= 0 {
			break
		}
	}
	// Unless we send the entire trie, we need to supply proofs
	// Actually, we need to supply proofs either way! This seems to be an implementation
	// quirk in go-ethereum
	proof := trienode.NewProofSet()
	if err := t.accountTrie.Prove(origin[:], proof); err != nil {
		t.logger.Error("Could not prove inexistence of origin", "origin", origin, "error", err)
	}

	if len(keys) > 0 {
		lastK := (keys[len(keys)-1])[:]
		if err := t.accountTrie.Prove(lastK, proof); err != nil {
			t.logger.Error("Could not prove last item", "error", err)
		}
	}
	return keys, vals, proof.List()
}

// defaultStorageRequestHandler is a well-behaving storage request handler
func defaultStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max uint64) error {
	hashes, slots, proofs := createStorageRequestResponse(t, root, accounts, bOrigin, bLimit, max)
	if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil {
		t.test.Errorf("Remote side rejected our delivery: %v", err)
		t.term()
	}

	return nil
}

func defaultCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error {
	var bytecodes [][]byte
	for _, h := range hashes {
		bytecodes = append(bytecodes, getCodeByHash(h))
	}

	if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil {
		t.test.Errorf("Remote side rejected our delivery: %v", err)
		t.term()
	}

	return nil
}

func createStorageRequestResponse(t *testPeer, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) {
	var size uint64

	for _, account := range accounts {
		// The first account might start from a different origin and end sooner
		var originHash common.Hash
		if len(origin) > 0 {
			originHash = common.BytesToHash(origin)
		}
		var limitHash = common.MaxHash
		if len(limit) > 0 {
			limitHash = common.BytesToHash(limit)
		}

		var (
			keys  []common.Hash
			vals  [][]byte
			abort bool
		)

		for _, entry := range t.storageValues[account] {
			if size >= max {
				abort = true
				break
			}

			if bytes.Compare(entry.k, originHash[:]) < 0 {
				continue
			}

			keys = append(keys, common.BytesToHash(entry.k))
			vals = append(vals, entry.v)
			size += uint64(32 + len(entry.v))

			if bytes.Compare(entry.k, limitHash[:]) >= 0 {
				break
			}
		}

		if len(keys) > 0 {
			hashes = append(hashes, keys)
			slots = append(slots, vals)
		}
		// Generate the Merkle proofs for the first and last storage slot, but
		// only if the response was capped. If the entire storage trie included
		// in the response, no need for any proofs.
		if originHash != (common.Hash{}) || (abort && len(keys) > 0) {
			// If we're aborting, we need to prove the first and last item
			// This terminates the response (and thus the loop)
			proof := trienode.NewProofSet()
			stTrie := t.storageTries[account]

			// Here's a potential gotcha: when constructing the proof, we cannot
			// use the 'origin' slice directly, but must use the full 32-byte
			// hash form.
			if err := stTrie.Prove(originHash[:], proof); err != nil {
				t.logger.Error("Could not prove inexistence of origin", "origin", originHash, "error", err)
			}

			if len(keys) > 0 {
				lastK := (keys[len(keys)-1])[:]
				if err := stTrie.Prove(lastK, proof); err != nil {
					t.logger.Error("Could not prove last item", "error", err)
				}
			}
			proofs = append(proofs, proof.List()...)
			break
		}
	}

	return hashes, slots, proofs
}

// createStorageRequestResponseAlwaysProve tests a cornercase, where the peer always
// supplies the proof for the last account, even if it is 'complete'.
func createStorageRequestResponseAlwaysProve(t *testPeer, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max uint64) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) {
	var size uint64

	max = max * 3 / 4

	var origin common.Hash
	if len(bOrigin) > 0 {
		origin = common.BytesToHash(bOrigin)
	}

	var exit bool

	for i, account := range accounts {
		var keys []common.Hash

		var vals [][]byte

		for _, entry := range t.storageValues[account] {
			if bytes.Compare(entry.k, origin[:]) < 0 {
				exit = true
			}

			keys = append(keys, common.BytesToHash(entry.k))
			vals = append(vals, entry.v)

			size += uint64(32 + len(entry.v))
			if size > max {
				exit = true
			}
		}

		if i == len(accounts)-1 {
			exit = true
		}

		hashes = append(hashes, keys)
		slots = append(slots, vals)

		if exit {
			// If we're aborting, we need to prove the first and last item
			// This terminates the response (and thus the loop)
			proof := trienode.NewProofSet()
			stTrie := t.storageTries[account]

			// Here's a potential gotcha: when constructing the proof, we cannot
			// use the 'origin' slice directly, but must use the full 32-byte
			// hash form.
			if err := stTrie.Prove(origin[:], proof); err != nil {
				t.logger.Error("Could not prove inexistence of origin", "origin", origin,
					"error", err)
			}

			if len(keys) > 0 {
				lastK := (keys[len(keys)-1])[:]
				if err := stTrie.Prove(lastK, proof); err != nil {
					t.logger.Error("Could not prove last item", "error", err)
				}
			}
			proofs = append(proofs, proof.List()...)
			break
		}
	}

	return hashes, slots, proofs
}

// emptyRequestAccountRangeFn is a rejects AccountRangeRequests
func emptyRequestAccountRangeFn(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error {
	t.remote.OnAccounts(t, requestId, nil, nil, nil)
	return nil
}

func nonResponsiveRequestAccountRangeFn(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error {
	return nil
}

func emptyTrieRequestHandler(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error {
	t.remote.OnTrieNodes(t, requestId, nil)
	return nil
}

func nonResponsiveTrieRequestHandler(t *testPeer, requestId uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error {
	return nil
}

func emptyStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error {
	t.remote.OnStorage(t, requestId, nil, nil, nil)
	return nil
}

func nonResponsiveStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error {
	return nil
}

func proofHappyStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error {
	hashes, slots, proofs := createStorageRequestResponseAlwaysProve(t, root, accounts, origin, limit, max)
	if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil {
		t.test.Errorf("Remote side rejected our delivery: %v", err)
		t.term()
	}

	return nil
}

//func emptyCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error {
//	var bytecodes [][]byte
//	t.remote.OnByteCodes(t, id, bytecodes)
//	return nil
//}

func corruptCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error {
	var bytecodes [][]byte
	for _, h := range hashes {
		// Send back the hashes
		bytecodes = append(bytecodes, h[:])
	}

	if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil {
		t.logger.Info("remote error on delivery (as expected)", "error", err)
		// Mimic the real-life handler, which drops a peer on errors
		t.remote.Unregister(t.id)
	}

	return nil
}

func cappedCodeRequestHandler(t *testPeer, id uint64, hashes []common.Hash, max uint64) error {
	var bytecodes [][]byte
	for _, h := range hashes[:1] {
		bytecodes = append(bytecodes, getCodeByHash(h))
	}
	// Missing bytecode can be retrieved again, no error expected
	if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil {
		t.test.Errorf("Remote side rejected our delivery: %v", err)
		t.term()
	}

	return nil
}

// starvingStorageRequestHandler is somewhat well-behaving storage handler, but it caps the returned results to be very small
func starvingStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error {
	return defaultStorageRequestHandler(t, requestId, root, accounts, origin, limit, 500)
}

func starvingAccountRequestHandler(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error {
	return defaultAccountRequestHandler(t, requestId, root, origin, limit, 500)
}

//func misdeliveringAccountRequestHandler(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, cap uint64) error {
//	return defaultAccountRequestHandler(t, requestId-1, root, origin, 500)
//}

func corruptAccountRequestHandler(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error {
	hashes, accounts, proofs := createAccountRequestResponse(t, root, origin, limit, cap)
	if len(proofs) > 0 {
		proofs = proofs[1:]
	}

	if err := t.remote.OnAccounts(t, requestId, hashes, accounts, proofs); err != nil {
		t.logger.Info("remote error on delivery (as expected)", "error", err)
		// Mimic the real-life handler, which drops a peer on errors
		t.remote.Unregister(t.id)
	}

	return nil
}

// corruptStorageRequestHandler doesn't provide good proofs
func corruptStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error {
	hashes, slots, proofs := createStorageRequestResponse(t, root, accounts, origin, limit, max)
	if len(proofs) > 0 {
		proofs = proofs[1:]
	}

	if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil {
		t.logger.Info("remote error on delivery (as expected)", "error", err)
		// Mimic the real-life handler, which drops a peer on errors
		t.remote.Unregister(t.id)
	}

	return nil
}

func noProofStorageRequestHandler(t *testPeer, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error {
	hashes, slots, _ := createStorageRequestResponse(t, root, accounts, origin, limit, max)
	if err := t.remote.OnStorage(t, requestId, hashes, slots, nil); err != nil {
		t.logger.Info("remote error on delivery (as expected)", "error", err)
		// Mimic the real-life handler, which drops a peer on errors
		t.remote.Unregister(t.id)
	}

	return nil
}

// TestSyncBloatedProof tests a scenario where we provide only _one_ value, but
// also ship the entire trie inside the proof. If the attack is successful,
// the remote side does not do any follow-up requests
func TestSyncBloatedProof(t *testing.T) {
	t.Parallel()

	testSyncBloatedProof(t, rawdb.HashScheme)
	testSyncBloatedProof(t, rawdb.PathScheme)
}

func testSyncBloatedProof(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)
	source := newTestPeer("source", t, term)
	source.accountTrie = sourceAccountTrie.Copy()
	source.accountValues = elems

	source.accountRequestHandler = func(t *testPeer, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error {
		var (
			keys []common.Hash
			vals [][]byte
		)
		// The values
		for _, entry := range t.accountValues {
			if bytes.Compare(entry.k, origin[:]) < 0 {
				continue
			}

			if bytes.Compare(entry.k, limit[:]) > 0 {
				continue
			}

			keys = append(keys, common.BytesToHash(entry.k))
			vals = append(vals, entry.v)
		}
		// The proofs
		proof := trienode.NewProofSet()
		if err := t.accountTrie.Prove(origin[:], proof); err != nil {
			t.logger.Error("Could not prove origin", "origin", origin, "error", err)
			t.logger.Error("Could not prove origin", "origin", origin, "error", err)
		}
		// The bloat: add proof of every single element
		for _, entry := range t.accountValues {
			if err := t.accountTrie.Prove(entry.k, proof); err != nil {
				t.logger.Error("Could not prove item", "error", err)
			}
		}
		// And remove one item from the elements
		if len(keys) > 2 {
			keys = append(keys[:1], keys[2:]...)
			vals = append(vals[:1], vals[2:]...)
		}
		if err := t.remote.OnAccounts(t, requestId, keys, vals, proof.List()); err != nil {
			t.logger.Info("remote error on delivery (as expected)", "error", err)
			t.term()
			// This is actually correct, signal to exit the test successfully
		}

		return nil
	}

	syncer := setupSyncer(nodeScheme, source)
	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err == nil {
		t.Fatal("No error returned from incomplete/cancelled sync")
	}
}

func setupSyncer(scheme string, peers ...*testPeer) *Syncer {
	stateDb := rawdb.NewMemoryDatabase()

	syncer := NewSyncer(stateDb, scheme)
	for _, peer := range peers {
		syncer.Register(peer)
		peer.remote = syncer
	}

	return syncer
}

// TestSync tests a basic sync with one peer
func TestSync(t *testing.T) {
	t.Parallel()

	testSync(t, rawdb.HashScheme)
	testSync(t, rawdb.PathScheme)
}

func testSync(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems

		return source
	}

	syncer := setupSyncer(nodeScheme, mkSource("source"))
	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncTinyTriePanic tests a basic sync with one peer, and a tiny trie. This caused a
// panic within the prover
func TestSyncTinyTriePanic(t *testing.T) {
	t.Parallel()

	testSyncTinyTriePanic(t, rawdb.HashScheme)
	testSyncTinyTriePanic(t, rawdb.PathScheme)
}

func testSyncTinyTriePanic(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(1, scheme)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems

		return source
	}
	syncer := setupSyncer(nodeScheme, mkSource("source"))
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestMultiSync tests a basic sync with multiple peers
func TestMultiSync(t *testing.T) {
	t.Parallel()

	testMultiSync(t, rawdb.HashScheme)
	testMultiSync(t, rawdb.PathScheme)
}

func testMultiSync(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems

		return source
	}
	syncer := setupSyncer(nodeScheme, mkSource("sourceA"), mkSource("sourceB"))
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncWithStorage tests  basic sync using accounts + storage + code
func TestSyncWithStorage(t *testing.T) {
	t.Parallel()

	testSyncWithStorage(t, rawdb.HashScheme)
	testSyncWithStorage(t, rawdb.PathScheme)
}

func testSyncWithStorage(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 3000, true, false, false)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems

		return source
	}
	syncer := setupSyncer(scheme, mkSource("sourceA"))
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestMultiSyncManyUseless contains one good peer, and many which doesn't return anything valuable at all
func TestMultiSyncManyUseless(t *testing.T) {
	t.Parallel()

	testMultiSyncManyUseless(t, rawdb.HashScheme)
	testMultiSyncManyUseless(t, rawdb.PathScheme)
}

func testMultiSyncManyUseless(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false)

	mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems

		if !noAccount {
			source.accountRequestHandler = emptyRequestAccountRangeFn
		}

		if !noStorage {
			source.storageRequestHandler = emptyStorageRequestHandler
		}

		if !noTrieNode {
			source.trieRequestHandler = emptyTrieRequestHandler
		}

		return source
	}

	syncer := setupSyncer(
		scheme,
		mkSource("full", true, true, true),
		mkSource("noAccounts", false, true, true),
		mkSource("noStorage", true, false, true),
		mkSource("noTrie", true, true, false),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestMultiSyncManyUselessWithLowTimeout contains one good peer, and many which doesn't return anything valuable at all
func TestMultiSyncManyUselessWithLowTimeout(t *testing.T) {
	t.Parallel()

	testMultiSyncManyUselessWithLowTimeout(t, rawdb.HashScheme)
	testMultiSyncManyUselessWithLowTimeout(t, rawdb.PathScheme)
}

func testMultiSyncManyUselessWithLowTimeout(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false)

	mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems

		if !noAccount {
			source.accountRequestHandler = emptyRequestAccountRangeFn
		}

		if !noStorage {
			source.storageRequestHandler = emptyStorageRequestHandler
		}

		if !noTrieNode {
			source.trieRequestHandler = emptyTrieRequestHandler
		}

		return source
	}

	syncer := setupSyncer(
		scheme,
		mkSource("full", true, true, true),
		mkSource("noAccounts", false, true, true),
		mkSource("noStorage", true, false, true),
		mkSource("noTrie", true, true, false),
	)
	// We're setting the timeout to very low, to increase the chance of the timeout
	// being triggered. This was previously a cause of panic, when a response
	// arrived simultaneously as a timeout was triggered.
	syncer.rates.OverrideTTLLimit = time.Millisecond

	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestMultiSyncManyUnresponsive contains one good peer, and many which doesn't respond at all
func TestMultiSyncManyUnresponsive(t *testing.T) {
	t.Parallel()

	testMultiSyncManyUnresponsive(t, rawdb.HashScheme)
	testMultiSyncManyUnresponsive(t, rawdb.PathScheme)
}

func testMultiSyncManyUnresponsive(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false)

	mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems

		if !noAccount {
			source.accountRequestHandler = nonResponsiveRequestAccountRangeFn
		}

		if !noStorage {
			source.storageRequestHandler = nonResponsiveStorageRequestHandler
		}

		if !noTrieNode {
			source.trieRequestHandler = nonResponsiveTrieRequestHandler
		}

		return source
	}

	syncer := setupSyncer(
		scheme,
		mkSource("full", true, true, true),
		mkSource("noAccounts", false, true, true),
		mkSource("noStorage", true, false, true),
		mkSource("noTrie", true, true, false),
	)
	// We're setting the timeout to very low, to make the test run a bit faster
	syncer.rates.OverrideTTLLimit = time.Millisecond

	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

func checkStall(t *testing.T, term func()) chan struct{} {
	testDone := make(chan struct{})

	go func() {
		select {
		case <-time.After(time.Minute): // TODO(karalabe): Make tests smaller, this is too much
			t.Log("Sync stalled")
			term()
		case <-testDone:
			return
		}
	}()

	return testDone
}

// TestSyncBoundaryAccountTrie tests sync against a few normal peers, but the
// account trie has a few boundary elements.
func TestSyncBoundaryAccountTrie(t *testing.T) {
	t.Parallel()

	testSyncBoundaryAccountTrie(t, rawdb.HashScheme)
	testSyncBoundaryAccountTrie(t, rawdb.PathScheme)
}

func testSyncBoundaryAccountTrie(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeBoundaryAccountTrie(scheme, 3000)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems

		return source
	}
	syncer := setupSyncer(
		nodeScheme,
		mkSource("peer-a"),
		mkSource("peer-b"),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncNoStorageAndOneCappedPeer tests sync using accounts and no storage, where one peer is
// consistently returning very small results
func TestSyncNoStorageAndOneCappedPeer(t *testing.T) {
	t.Parallel()

	testSyncNoStorageAndOneCappedPeer(t, rawdb.HashScheme)
	testSyncNoStorageAndOneCappedPeer(t, rawdb.PathScheme)
}

func testSyncNoStorageAndOneCappedPeer(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)

	mkSource := func(name string, slow bool) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems

		if slow {
			source.accountRequestHandler = starvingAccountRequestHandler
		}

		return source
	}

	syncer := setupSyncer(
		nodeScheme,
		mkSource("nice-a", false),
		mkSource("nice-b", false),
		mkSource("nice-c", false),
		mkSource("capped", true),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncNoStorageAndOneCodeCorruptPeer has one peer which doesn't deliver
// code requests properly.
func TestSyncNoStorageAndOneCodeCorruptPeer(t *testing.T) {
	t.Parallel()

	testSyncNoStorageAndOneCodeCorruptPeer(t, rawdb.HashScheme)
	testSyncNoStorageAndOneCodeCorruptPeer(t, rawdb.PathScheme)
}

func testSyncNoStorageAndOneCodeCorruptPeer(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)

	mkSource := func(name string, codeFn codeHandlerFunc) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.codeRequestHandler = codeFn

		return source
	}
	// One is capped, one is corrupt. If we don't use a capped one, there's a 50%
	// chance that the full set of codes requested are sent only to the
	// non-corrupt peer, which delivers everything in one go, and makes the
	// test moot
	syncer := setupSyncer(
		nodeScheme,
		mkSource("capped", cappedCodeRequestHandler),
		mkSource("corrupt", corruptCodeRequestHandler),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

func TestSyncNoStorageAndOneAccountCorruptPeer(t *testing.T) {
	t.Parallel()

	testSyncNoStorageAndOneAccountCorruptPeer(t, rawdb.HashScheme)
	testSyncNoStorageAndOneAccountCorruptPeer(t, rawdb.PathScheme)
}

func testSyncNoStorageAndOneAccountCorruptPeer(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)

	mkSource := func(name string, accFn accountHandlerFunc) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.accountRequestHandler = accFn

		return source
	}
	// One is capped, one is corrupt. If we don't use a capped one, there's a 50%
	// chance that the full set of codes requested are sent only to the
	// non-corrupt peer, which delivers everything in one go, and makes the
	// test moot
	syncer := setupSyncer(
		nodeScheme,
		mkSource("capped", defaultAccountRequestHandler),
		mkSource("corrupt", corruptAccountRequestHandler),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncNoStorageAndOneCodeCappedPeer has one peer which delivers code hashes
// one by one
func TestSyncNoStorageAndOneCodeCappedPeer(t *testing.T) {
	t.Parallel()

	testSyncNoStorageAndOneCodeCappedPeer(t, rawdb.HashScheme)
	testSyncNoStorageAndOneCodeCappedPeer(t, rawdb.PathScheme)
}

func testSyncNoStorageAndOneCodeCappedPeer(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme)

	mkSource := func(name string, codeFn codeHandlerFunc) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.codeRequestHandler = codeFn

		return source
	}
	// Count how many times it's invoked. Remember, there are only 8 unique hashes,
	// so it shouldn't be more than that
	var counter int

	syncer := setupSyncer(
		nodeScheme,
		mkSource("capped", func(t *testPeer, id uint64, hashes []common.Hash, max uint64) error {
			counter++
			return cappedCodeRequestHandler(t, id, hashes, max)
		}),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)

	// There are only 8 unique hashes, and 3K accounts. However, the code
	// deduplication is per request batch. If it were a perfect global dedup,
	// we would expect only 8 requests. If there were no dedup, there would be
	// 3k requests.
	// We expect somewhere below 100 requests for these 8 unique hashes. But
	// the number can be flaky, so don't limit it so strictly.
	if threshold := 100; counter > threshold {
		t.Logf("Error, expected < %d invocations, got %d", threshold, counter)
	}
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncBoundaryStorageTrie tests sync against a few normal peers, but the
// storage trie has a few boundary elements.
func TestSyncBoundaryStorageTrie(t *testing.T) {
	t.Parallel()

	testSyncBoundaryStorageTrie(t, rawdb.HashScheme)
	testSyncBoundaryStorageTrie(t, rawdb.PathScheme)
}

func testSyncBoundaryStorageTrie(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 10, 1000, false, true, false)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems

		return source
	}
	syncer := setupSyncer(
		scheme,
		mkSource("peer-a"),
		mkSource("peer-b"),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncWithStorageAndOneCappedPeer tests sync using accounts + storage, where one peer is
// consistently returning very small results
func TestSyncWithStorageAndOneCappedPeer(t *testing.T) {
	t.Parallel()

	testSyncWithStorageAndOneCappedPeer(t, rawdb.HashScheme)
	testSyncWithStorageAndOneCappedPeer(t, rawdb.PathScheme)
}

func testSyncWithStorageAndOneCappedPeer(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 300, 1000, false, false, false)

	mkSource := func(name string, slow bool) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems

		if slow {
			source.storageRequestHandler = starvingStorageRequestHandler
		}

		return source
	}

	syncer := setupSyncer(
		scheme,
		mkSource("nice-a", false),
		mkSource("slow", true),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncWithStorageAndCorruptPeer tests sync using accounts + storage, where one peer is
// sometimes sending bad proofs
func TestSyncWithStorageAndCorruptPeer(t *testing.T) {
	t.Parallel()

	testSyncWithStorageAndCorruptPeer(t, rawdb.HashScheme)
	testSyncWithStorageAndCorruptPeer(t, rawdb.PathScheme)
}

func testSyncWithStorageAndCorruptPeer(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false)

	mkSource := func(name string, handler storageHandlerFunc) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems
		source.storageRequestHandler = handler

		return source
	}

	syncer := setupSyncer(
		scheme,
		mkSource("nice-a", defaultStorageRequestHandler),
		mkSource("nice-b", defaultStorageRequestHandler),
		mkSource("nice-c", defaultStorageRequestHandler),
		mkSource("corrupt", corruptStorageRequestHandler),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

func TestSyncWithStorageAndNonProvingPeer(t *testing.T) {
	t.Parallel()

	testSyncWithStorageAndNonProvingPeer(t, rawdb.HashScheme)
	testSyncWithStorageAndNonProvingPeer(t, rawdb.PathScheme)
}

func testSyncWithStorageAndNonProvingPeer(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false)

	mkSource := func(name string, handler storageHandlerFunc) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems
		source.storageRequestHandler = handler

		return source
	}
	syncer := setupSyncer(
		scheme,
		mkSource("nice-a", defaultStorageRequestHandler),
		mkSource("nice-b", defaultStorageRequestHandler),
		mkSource("nice-c", defaultStorageRequestHandler),
		mkSource("corrupt", noProofStorageRequestHandler),
	)
	done := checkStall(t, term)

	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncWithStorageMisbehavingProve tests  basic sync using accounts + storage + code, against
// a peer who insists on delivering full storage sets _and_ proofs. This triggered
// an error, where the recipient erroneously clipped the boundary nodes, but
// did not mark the account for healing.
func TestSyncWithStorageMisbehavingProve(t *testing.T) {
	t.Parallel()

	testSyncWithStorageMisbehavingProve(t, rawdb.HashScheme)
	testSyncWithStorageMisbehavingProve(t, rawdb.PathScheme)
}

func testSyncWithStorageMisbehavingProve(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorageWithUniqueStorage(scheme, 10, 30, false)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems
		source.storageRequestHandler = proofHappyStorageRequestHandler

		return source
	}

	syncer := setupSyncer(nodeScheme, mkSource("sourceA"))
	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
}

// TestSyncWithUnevenStorage tests sync where the storage trie is not even
// and with a few empty ranges.
func TestSyncWithUnevenStorage(t *testing.T) {
	t.Parallel()

	testSyncWithUnevenStorage(t, rawdb.HashScheme)
	testSyncWithUnevenStorage(t, rawdb.PathScheme)
}

func testSyncWithUnevenStorage(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	accountTrie, accounts, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 256, false, false, true)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = accountTrie.Copy()
		source.accountValues = accounts
		source.setStorageTries(storageTries)
		source.storageValues = storageElems
		source.storageRequestHandler = func(t *testPeer, reqId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max uint64) error {
			return defaultStorageRequestHandler(t, reqId, root, accounts, origin, limit, 128) // retrieve storage in large mode
		}
		return source
	}
	syncer := setupSyncer(scheme, mkSource("source"))
	if err := syncer.Sync(accountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}
	verifyTrie(scheme, syncer.db, accountTrie.Hash(), t)
}

type kv struct {
	k, v []byte
}

func (k *kv) cmp(other *kv) int {
	return bytes.Compare(k.k, other.k)
}

func key32(i uint64) []byte {
	key := make([]byte, 32)
	binary.LittleEndian.PutUint64(key, i)

	return key
}

var (
	codehashes = []common.Hash{
		crypto.Keccak256Hash([]byte{0}),
		crypto.Keccak256Hash([]byte{1}),
		crypto.Keccak256Hash([]byte{2}),
		crypto.Keccak256Hash([]byte{3}),
		crypto.Keccak256Hash([]byte{4}),
		crypto.Keccak256Hash([]byte{5}),
		crypto.Keccak256Hash([]byte{6}),
		crypto.Keccak256Hash([]byte{7}),
	}
)

// getCodeHash returns a pseudo-random code hash
func getCodeHash(i uint64) []byte {
	h := codehashes[int(i)%len(codehashes)]
	return common.CopyBytes(h[:])
}

// getCodeByHash convenience function to lookup the code from the code hash
func getCodeByHash(hash common.Hash) []byte {
	if hash == types.EmptyCodeHash {
		return nil
	}

	for i, h := range codehashes {
		if h == hash {
			return []byte{byte(i)}
		}
	}

	return nil
}

// makeAccountTrieNoStorage spits out a trie, along with the leaves
func makeAccountTrieNoStorage(n int, scheme string) (string, *trie.Trie, []*kv) {
	var (
		db      = triedb.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme))
		accTrie = trie.NewEmpty(db)
		entries []*kv
	)

	for i := uint64(1); i <= uint64(n); i++ {
		value, _ := rlp.EncodeToBytes(&types.StateAccount{
			Nonce:    i,
			Balance:  uint256.NewInt(i),
			Root:     types.EmptyRootHash,
			CodeHash: getCodeHash(i),
		})
		key := key32(i)
		elem := &kv{key, value}
		accTrie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)
	}
	slices.SortFunc(entries, (*kv).cmp)

	// Commit the state changes into db and re-create the trie
	// for accessing later.
	root, nodes := accTrie.Commit(false)
	db.Update(root, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), triedb.NewStateSet())

	accTrie, _ = trie.New(trie.StateTrieID(root), db)

	return db.Scheme(), accTrie, entries
}

// makeBoundaryAccountTrie constructs an account trie. Instead of filling
// accounts normally, this function will fill a few accounts which have
// boundary hash.
func makeBoundaryAccountTrie(scheme string, n int) (string, *trie.Trie, []*kv) {
	var (
		entries    []*kv
		boundaries []common.Hash

		db      = triedb.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme))
		accTrie = trie.NewEmpty(db)
	)
	// Initialize boundaries
	var next common.Hash

	step := new(big.Int).Sub(
		new(big.Int).Div(
			new(big.Int).Exp(common.Big2, common.Big256, nil),
			big.NewInt(int64(accountConcurrency)),
		), common.Big1,
	)
	for i := 0; i < accountConcurrency; i++ {
		last := common.BigToHash(new(big.Int).Add(next.Big(), step))
		if i == accountConcurrency-1 {
			last = common.MaxHash
		}

		boundaries = append(boundaries, last)
		next = common.BigToHash(new(big.Int).Add(last.Big(), common.Big1))
	}
	// Fill boundary accounts
	for i := 0; i < len(boundaries); i++ {
		value, _ := rlp.EncodeToBytes(&types.StateAccount{
			Nonce:    uint64(0),
			Balance:  uint256.NewInt(uint64(i)),
			Root:     types.EmptyRootHash,
			CodeHash: getCodeHash(uint64(i)),
		})
		elem := &kv{boundaries[i].Bytes(), value}
		accTrie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)
	}
	// Fill other accounts if required
	for i := uint64(1); i <= uint64(n); i++ {
		value, _ := rlp.EncodeToBytes(&types.StateAccount{
			Nonce:    i,
			Balance:  uint256.NewInt(i),
			Root:     types.EmptyRootHash,
			CodeHash: getCodeHash(i),
		})
		elem := &kv{key32(i), value}
		accTrie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)
	}
	slices.SortFunc(entries, (*kv).cmp)

	// Commit the state changes into db and re-create the trie
	// for accessing later.
	root, nodes := accTrie.Commit(false)
	db.Update(root, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), triedb.NewStateSet())

	accTrie, _ = trie.New(trie.StateTrieID(root), db)

	return db.Scheme(), accTrie, entries
}

// makeAccountTrieWithStorageWithUniqueStorage creates an account trie where each accounts
// has a unique storage set.
func makeAccountTrieWithStorageWithUniqueStorage(scheme string, accounts, slots int, code bool) (string, *trie.Trie, []*kv, map[common.Hash]*trie.Trie, map[common.Hash][]*kv) {
	var (
		db             = triedb.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme))
		accTrie        = trie.NewEmpty(db)
		entries        []*kv
		storageRoots   = make(map[common.Hash]common.Hash)
		storageTries   = make(map[common.Hash]*trie.Trie)
		storageEntries = make(map[common.Hash][]*kv)
		nodes          = trienode.NewMergedNodeSet()
	)
	// Create n accounts in the trie
	for i := uint64(1); i <= uint64(accounts); i++ {
		key := key32(i)

		codehash := types.EmptyCodeHash.Bytes()
		if code {
			codehash = getCodeHash(i)
		}
		// Create a storage trie
		stRoot, stNodes, stEntries := makeStorageTrieWithSeed(common.BytesToHash(key), uint64(slots), i, db)
		_ = nodes.Merge(stNodes)

		value, _ := rlp.EncodeToBytes(&types.StateAccount{
			Nonce:    i,
			Balance:  uint256.NewInt(i),
			Root:     stRoot,
			CodeHash: codehash,
		})
		elem := &kv{key, value}
		accTrie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)

		storageRoots[common.BytesToHash(key)] = stRoot
		storageEntries[common.BytesToHash(key)] = stEntries
	}
	slices.SortFunc(entries, (*kv).cmp)

	// Commit account trie
	root, set := accTrie.Commit(true)
	nodes.Merge(set)

	// Commit gathered dirty nodes into database
	db.Update(root, types.EmptyRootHash, 0, nodes, triedb.NewStateSet())

	// Re-create tries with new root
	accTrie, _ = trie.New(trie.StateTrieID(root), db)

	for i := uint64(1); i <= uint64(accounts); i++ {
		key := key32(i)
		id := trie.StorageTrieID(root, common.BytesToHash(key), storageRoots[common.BytesToHash(key)])
		trie, _ := trie.New(id, db)
		storageTries[common.BytesToHash(key)] = trie
	}

	return db.Scheme(), accTrie, entries, storageTries, storageEntries
}

// makeAccountTrieWithStorage spits out a trie, along with the leaves
func makeAccountTrieWithStorage(scheme string, accounts, slots int, code, boundary bool, uneven bool) (*trie.Trie, []*kv, map[common.Hash]*trie.Trie, map[common.Hash][]*kv) {
	var (
		db             = triedb.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme))
		accTrie        = trie.NewEmpty(db)
		entries        []*kv
		storageRoots   = make(map[common.Hash]common.Hash)
		storageTries   = make(map[common.Hash]*trie.Trie)
		storageEntries = make(map[common.Hash][]*kv)
		nodes          = trienode.NewMergedNodeSet()
	)
	// Create n accounts in the trie
	for i := uint64(1); i <= uint64(accounts); i++ {
		key := key32(i)

		codehash := types.EmptyCodeHash.Bytes()
		if code {
			codehash = getCodeHash(i)
		}
		// Make a storage trie
		var (
			stRoot    common.Hash
			stNodes   *trienode.NodeSet
			stEntries []*kv
		)

		if boundary {
			stRoot, stNodes, stEntries = makeBoundaryStorageTrie(common.BytesToHash(key), slots, db)
		} else if uneven {
			stRoot, stNodes, stEntries = makeUnevenStorageTrie(common.BytesToHash(key), slots, db)
		} else {
			stRoot, stNodes, stEntries = makeStorageTrieWithSeed(common.BytesToHash(key), uint64(slots), 0, db)
		}

		_ = nodes.Merge(stNodes)

		value, _ := rlp.EncodeToBytes(&types.StateAccount{
			Nonce:    i,
			Balance:  uint256.NewInt(i),
			Root:     stRoot,
			CodeHash: codehash,
		})
		elem := &kv{key, value}
		accTrie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)

		// we reuse the same one for all accounts
		storageRoots[common.BytesToHash(key)] = stRoot
		storageEntries[common.BytesToHash(key)] = stEntries
	}
	slices.SortFunc(entries, (*kv).cmp)

	// Commit account trie
	root, set := accTrie.Commit(true)
	nodes.Merge(set)

	// Commit gathered dirty nodes into database
	db.Update(root, types.EmptyRootHash, 0, nodes, triedb.NewStateSet())

	// Re-create tries with new root
	accTrie, err := trie.New(trie.StateTrieID(root), db)
	if err != nil {
		panic(err)
	}

	for i := uint64(1); i <= uint64(accounts); i++ {
		key := key32(i)
		id := trie.StorageTrieID(root, common.BytesToHash(key), storageRoots[common.BytesToHash(key)])

		trie, err := trie.New(id, db)
		if err != nil {
			panic(err)
		}

		storageTries[common.BytesToHash(key)] = trie
	}
	return accTrie, entries, storageTries, storageEntries
}

// makeStorageTrieWithSeed fills a storage trie with n items, returning the
// not-yet-committed trie and the sorted entries. The seeds can be used to ensure
// that tries are unique.
func makeStorageTrieWithSeed(owner common.Hash, n, seed uint64, db *triedb.Database) (common.Hash, *trienode.NodeSet, []*kv) {
	trie, _ := trie.New(trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash), db)
	var entries []*kv
	for i := uint64(1); i <= n; i++ {
		// store 'x' at slot 'x'
		slotValue := key32(i + seed)
		rlpSlotValue, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(slotValue[:]))

		slotKey := key32(i)
		key := crypto.Keccak256Hash(slotKey[:])

		elem := &kv{key[:], rlpSlotValue}
		trie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)
	}
	slices.SortFunc(entries, (*kv).cmp)
	root, nodes := trie.Commit(false)
	return root, nodes, entries
}

// makeBoundaryStorageTrie constructs a storage trie. Instead of filling
// storage slots normally, this function will fill a few slots which have
// boundary hash.
func makeBoundaryStorageTrie(owner common.Hash, n int, db *triedb.Database) (common.Hash, *trienode.NodeSet, []*kv) {
	var (
		entries    []*kv
		boundaries []common.Hash
		trie, _    = trie.New(trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash), db)
	)
	// Initialize boundaries
	var next common.Hash

	step := new(big.Int).Sub(
		new(big.Int).Div(
			new(big.Int).Exp(common.Big2, common.Big256, nil),
			big.NewInt(int64(accountConcurrency)),
		), common.Big1,
	)
	for i := 0; i < accountConcurrency; i++ {
		last := common.BigToHash(new(big.Int).Add(next.Big(), step))
		if i == accountConcurrency-1 {
			last = common.MaxHash
		}

		boundaries = append(boundaries, last)
		next = common.BigToHash(new(big.Int).Add(last.Big(), common.Big1))
	}
	// Fill boundary slots
	for i := 0; i < len(boundaries); i++ {
		key := boundaries[i]
		val := []byte{0xde, 0xad, 0xbe, 0xef}

		elem := &kv{key[:], val}
		trie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)
	}
	// Fill other slots if required
	for i := uint64(1); i <= uint64(n); i++ {
		slotKey := key32(i)
		key := crypto.Keccak256Hash(slotKey[:])

		slotValue := key32(i)
		rlpSlotValue, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(slotValue[:]))

		elem := &kv{key[:], rlpSlotValue}
		trie.MustUpdate(elem.k, elem.v)
		entries = append(entries, elem)
	}
	slices.SortFunc(entries, (*kv).cmp)
	root, nodes := trie.Commit(false)
	return root, nodes, entries
}

// makeUnevenStorageTrie constructs a storage tries will states distributed in
// different range unevenly.
func makeUnevenStorageTrie(owner common.Hash, slots int, db *triedb.Database) (common.Hash, *trienode.NodeSet, []*kv) {
	var (
		entries []*kv
		tr, _   = trie.New(trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash), db)
		chosen  = make(map[byte]struct{})
	)
	for i := 0; i < 3; i++ {
		var n int
		for {
			n = mrand.Intn(15) // the last range is set empty deliberately
			if _, ok := chosen[byte(n)]; ok {
				continue
			}
			chosen[byte(n)] = struct{}{}
			break
		}
		for j := 0; j < slots/3; j++ {
			key := append([]byte{byte(n)}, testrand.Bytes(31)...)
			val, _ := rlp.EncodeToBytes(testrand.Bytes(32))

			elem := &kv{key, val}
			tr.MustUpdate(elem.k, elem.v)
			entries = append(entries, elem)
		}
	}
	slices.SortFunc(entries, (*kv).cmp)
	root, nodes := tr.Commit(false)
	return root, nodes, entries
}

func verifyTrie(scheme string, db ethdb.KeyValueStore, root common.Hash, t *testing.T) {
	t.Helper()
	triedb := triedb.NewDatabase(rawdb.NewDatabase(db), newDbConfig(scheme))
	accTrie, err := trie.New(trie.StateTrieID(root), triedb)
	if err != nil {
		t.Fatal(err)
	}

	accounts, slots := 0, 0
	accIt := trie.NewIterator(accTrie.MustNodeIterator(nil))
	for accIt.Next() {
		var acc struct {
			Nonce    uint64
			Balance  *big.Int
			Root     common.Hash
			CodeHash []byte
		}

		if err := rlp.DecodeBytes(accIt.Value, &acc); err != nil {
			log.Crit("Invalid account encountered during snapshot creation", "err", err)
		}

		accounts++

		if acc.Root != types.EmptyRootHash {
			id := trie.StorageTrieID(root, common.BytesToHash(accIt.Key), acc.Root)

			storeTrie, err := trie.NewStateTrie(id, triedb)
			if err != nil {
				t.Fatal(err)
			}
			storeIt := trie.NewIterator(storeTrie.MustNodeIterator(nil))
			for storeIt.Next() {
				slots++
			}

			if err := storeIt.Err; err != nil {
				t.Fatal(err)
			}
		}
	}

	if err := accIt.Err; err != nil {
		t.Fatal(err)
	}

	t.Logf("accounts: %d, slots: %d", accounts, slots)
}

// TestSyncAccountPerformance tests how efficient the snap algo is at minimizing
// state healing
func TestSyncAccountPerformance(t *testing.T) {
	// These tests must not run in parallel: they modify the
	// global var accountConcurrency
	t.Skip("TODO: Does not return expected results, needs to be fixed")

	testSyncAccountPerformance(t, rawdb.HashScheme)
	testSyncAccountPerformance(t, rawdb.PathScheme)
}

func testSyncAccountPerformance(t *testing.T, scheme string) {
	// Set the account concurrency to 1. This _should_ result in the
	// range root to become correct, and there should be no healing needed
	defer func(old int) { accountConcurrency = old }(accountConcurrency)
	accountConcurrency = 1

	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)
	nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems

		return source
	}
	src := mkSource("source")

	syncer := setupSyncer(nodeScheme, src)
	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}
	verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t)
	// The trie root will always be requested, since it is added when the snap
	// sync cycle starts. When popping the queue, we do not look it up again.
	// Doing so would bring this number down to zero in this artificial testcase,
	// but only add extra IO for no reason in practice.
	if have, want := src.nTrienodeRequests, 1; have != want {
		fmt.Print(src.Stats())
		t.Errorf("trie node heal requests wrong, want %d, have %d", want, have)
	}
}

func TestSlotEstimation(t *testing.T) {
	for i, tc := range []struct {
		last  common.Hash
		count int
		want  uint64
	}{
		{
			// Half the space
			common.HexToHash("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
			100,
			100,
		},
		{
			// 1 / 16th
			common.HexToHash("0x0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
			100,
			1500,
		},
		{
			// Bit more than 1 / 16th
			common.HexToHash("0x1000000000000000000000000000000000000000000000000000000000000000"),
			100,
			1499,
		},
		{
			// Almost everything
			common.HexToHash("0xF000000000000000000000000000000000000000000000000000000000000000"),
			100,
			6,
		},
		{
			// Almost nothing -- should lead to error
			common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"),
			1,
			0,
		},
		{
			// Nothing -- should lead to error
			common.Hash{},
			100,
			0,
		},
	} {
		have, _ := estimateRemainingSlots(tc.count, tc.last)
		if want := tc.want; have != want {
			t.Errorf("test %d: have %d want %d", i, have, want)
		}
	}
}

func newDbConfig(scheme string) *triedb.Config {
	if scheme == rawdb.HashScheme {
		return &triedb.Config{}
	}
	return &triedb.Config{PathDB: &pathdb.Config{SnapshotNoBuild: true}}
}

// TestBytecodeOnlyMode tests the bytecode-only sync mode
func TestBytecodeOnlyMode(t *testing.T) {
	t.Parallel()

	testBytecodeOnlyMode(t, rawdb.HashScheme)
	testBytecodeOnlyMode(t, rawdb.PathScheme)
}

func testBytecodeOnlyMode(t *testing.T, scheme string) {
	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)

	// Create a test trie with accounts and code
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 10, 100, true, false, false)

	// Create source peer
	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems
		return source
	}

	// Setup syncer with bytecode-only mode
	syncer := setupSyncer(scheme, mkSource("source"))
	syncer.SetBytecodeOnlyMode(true)

	done := checkStall(t, term)

	// Start sync
	if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil {
		t.Fatalf("sync failed: %v", err)
	}

	close(done)

	// In bytecode-only mode, the sync should complete but with focus on bytecodes
	t.Log("Bytecode-only sync completed successfully")
}

// TestBytecodeOnlyModeSimple tests basic bytecode-only sync functionality
func TestBytecodeOnlyModeSimple(t *testing.T) {
	t.Parallel()

	// Test that SetBytecodeOnlyMode can be called
	scheme := rawdb.HashScheme
	db := rawdb.NewMemoryDatabase()
	syncer := NewSyncer(db, scheme)

	// Enable bytecode-only mode
	syncer.SetBytecodeOnlyMode(true)

	// Verify the mode is set
	syncer.lock.Lock()
	if !syncer.bytecodeOnlyMode {
		t.Error("Expected bytecode-only mode to be enabled")
	}
	syncer.lock.Unlock()

	// Disable bytecode-only mode
	syncer.SetBytecodeOnlyMode(false)

	// Verify the mode is unset
	syncer.lock.Lock()
	if syncer.bytecodeOnlyMode {
		t.Error("Expected bytecode-only mode to be disabled")
	}
	syncer.lock.Unlock()
}

// TestBytecodeOnlyModeFiltering tests that bytecode-only mode affects sync behavior
func TestBytecodeOnlyModeFiltering(t *testing.T) {
	t.Parallel()

	var (
		once   sync.Once
		cancel = make(chan struct{})
		term   = func() {
			once.Do(func() {
				close(cancel)
			})
		}
	)

	// Create test data
	scheme := rawdb.HashScheme
	sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 5, 50, true, true, false)

	// Track what data the peer serves
	var (
		accountRangeRequests int
		storageRangeRequests int
		bytecodeRequests     int
		trienodeRequests     int
	)

	mkSource := func(name string) *testPeer {
		source := newTestPeer(name, t, term)
		source.accountTrie = sourceAccountTrie.Copy()
		source.accountValues = elems
		source.setStorageTries(storageTries)
		source.storageValues = storageElems

		// Wrap handlers to count requests
		source.accountRequestHandler = func(peer *testPeer, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap uint64) error {
			accountRangeRequests++
			return defaultAccountRequestHandler(peer, id, root, origin, limit, cap)
		}
		source.storageRequestHandler = func(peer *testPeer, id uint64, root common.Hash, accounts []common.Hash, origin []byte, limit []byte, cap uint64) error {
			storageRangeRequests++
			return defaultStorageRequestHandler(peer, id, root, accounts, origin, limit, cap)
		}
		source.codeRequestHandler = func(peer *testPeer, id uint64, hashes []common.Hash, cap uint64) error {
			bytecodeRequests++
			return defaultCodeRequestHandler(peer, id, hashes, cap)
		}
		source.trieRequestHandler = func(peer *testPeer, id uint64, root common.Hash, paths []TrieNodePathSet, cap uint64) error {
			trienodeRequests++
			return defaultTrieRequestHandler(peer, id, root, paths, cap)
		}

		return source
	}

	// Setup syncer with bytecode-only mode
	syncer := setupSyncer(scheme, mkSource("source"))
	syncer.SetBytecodeOnlyMode(true)

	// Run sync for a short time to see requests
	go func() {
		time.Sleep(100 * time.Millisecond)
		close(cancel)
	}()

	// Start sync (may not complete, but we'll see requests)
	syncer.Sync(sourceAccountTrie.Hash(), cancel)

	// Log request counts
	t.Logf("Request counts - Accounts: %d, Storage: %d, Bytecode: %d, TrieNodes: %d",
		accountRangeRequests, storageRangeRequests, bytecodeRequests, trienodeRequests)

	// In bytecode-only mode, we expect to see account requests
	// (to find contracts) and potentially bytecode requests
	if accountRangeRequests == 0 {
		t.Log("Note: No account requests seen (sync may have terminated early)")
	}
}
